package com.jprocms.module.cms.util;

import com.jpro.framework.common.exception.ServerException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*;

import static com.jpro.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.jprocms.module.cms.enums.ErrorCodeConstants.COMMON_PARAM_ERROR;

/**
 * @author jprocms
 */
@Slf4j
public class FileUtil {
    private static final String SPECIALS1 = "\0";
    /**
     * 压缩包后缀
     */
    public static final String[] ZIP_EXT = new String[]{"zip", "rar", "tar", "gzip"};
    private static final long KB = 1024;
    private static final int THRESHOLD_10 = 10;
    private static final int THRESHOLD_100 = 100;
    private static final int THRESHOLD_900 = 900;
    private static Map<String, String> fileTypeMap = new HashMap<>();
    private static Map<String, List<String>> whiteList = new HashMap<>();
    private static List<String> blackList = new ArrayList<>();
    private static List<String> htmlHeaders = new ArrayList<>();
    private static String fileTempPath;

    static {
        // 初始化文件类型信息
        initFileTypeHeadInfos();
    }

    private static void initFileTypeHeadInfos() {
        List<String> jpgHeadList = new ArrayList<String>();
        jpgHeadList.add("ffd8ff");
        List<String> pngHeadList = new ArrayList<String>();
        pngHeadList.add("89504e");
        pngHeadList.add("89504e47");
        List<String> gifHeadList = new ArrayList<String>();
        gifHeadList.add("47494638");
        gifHeadList.add("4749463837");
        gifHeadList.add("4749463839");
        List<String> bmpHeadList = new ArrayList<String>();
        bmpHeadList.add("424d");
        List<String> aviHeadList = new ArrayList<String>();
        aviHeadList.add("41564920");
        aviHeadList.add("52494646");
        List<String> wmvHeadList = new ArrayList<String>();
        wmvHeadList.add("3026b2758e66cf11a6d9");
        List<String> mp4HeadList = new ArrayList<String>();
        mp4HeadList.add("000000");
        List<String> pdfHeadList = new ArrayList<String>();
        pdfHeadList.add("255044462d312e");
        List<String> docHeadList = new ArrayList<String>();
        docHeadList.add("d0cf11e0a1b11ae10000");
        docHeadList.add("d0cf11e0a1b11ae10");
        List<String> docxHeadList = new ArrayList<String>();
        docxHeadList.add("504b03041400");
        List<String> htmlHeadList = new ArrayList<String>();
        htmlHeadList.add("68746D6C3E");
        htmlHeadList.add("3c21444f435459504520");
        List<String> flvHeadList = new ArrayList<String>();
        flvHeadList.add("464c56");
        List<String> tifHeadList = new ArrayList<String>();
        tifHeadList.add("49492A00");
        List<String> rarHeadList = new ArrayList<String>();
        rarHeadList.add("52617221");
        rarHeadList.add("1f8b0800");
        rarHeadList.add("504b0304140000000800");
        rarHeadList.add("526172211a0700cf9073");
        //zip
        rarHeadList.add("504B0304");
        //gz
        rarHeadList.add("1f8b0800000000000000");
        // TIFF (tif)
        gifHeadList.add("49492a00227105008037");
        //CAD (dwg)
        gifHeadList.add("41433130313500000000");
        // Rich Text Format (rtf)
        gifHeadList.add("7b5c727466315c616e73");
        // Photoshop (psd)
        gifHeadList.add("38425053000100000000");
        //eml
        rarHeadList.add("46726f6d3a203d3f6762");
        //mdb
        rarHeadList.add("5374616E64617264204A");
        //ps
        rarHeadList.add("252150532D41646F6265");
        // rmvb/rm相同
        wmvHeadList.add("2e524d46000000120001");
        // flv与f4v相同
        wmvHeadList.add("464c5601050000000900");
        //mp3
        wmvHeadList.add("49443303000000002176");
        //mpg
        wmvHeadList.add("000001ba210001000180");
        //Wave (wav)
        wmvHeadList.add("52494646e27807005741");
        //mid
        wmvHeadList.add("4d546864000000060001");
        // Quicktime (mov)
        wmvHeadList.add("6D6F6F76");
        // Outlook Express (dbx)
        wmvHeadList.add("CFAD12FEC5FD746F");
        // Outlook (pst)
        wmvHeadList.add("2142444E");
        // Quicken (qdf)
        wmvHeadList.add("AC9EBD8F");
        // Windows Password (pwl)
        wmvHeadList.add("E3828596");
        //Real Audio (ram)
        wmvHeadList.add("2E7261FD");

        whiteList.put("jpg", jpgHeadList);
        whiteList.put("jpeg", jpgHeadList);
        whiteList.put("png", pngHeadList);
        whiteList.put("gif", gifHeadList);
        whiteList.put("bmp", bmpHeadList);
        whiteList.put("avi", aviHeadList);
        whiteList.put("wmv", wmvHeadList);
        whiteList.put("mp4", mp4HeadList);
        whiteList.put("pdf", pdfHeadList);
        whiteList.put("doc", docHeadList);
        whiteList.put("xls", docHeadList);
        whiteList.put("ppt", docHeadList);
        whiteList.put("flv", flvHeadList);
        whiteList.put("docx", docxHeadList);
        whiteList.put("xlsx", docxHeadList);
        whiteList.put("pptx", docxHeadList);
        //whiteList.put("html", htmlHeadList);
        whiteList.put("tif", tifHeadList);
        whiteList.put("rar", rarHeadList);
        // txt 文件头过于多了
        whiteList.put("txt", null);
        whiteList.put("3gp", null);

        //黑名单 exe html htm jsp
        blackList.add("4d5a90");
        blackList.add("23212f");
        blackList.add("68746D6C3E");
        blackList.add("3c2540");
        blackList.add("3c21646f637479706520");
        blackList.add("3c21444f435459504520");

        htmlHeaders.add("3c21646f637479706520");
        htmlHeaders.add("3c21444f435459504520");

        //如果一定需要上传exe请使用下面注释的代码
//		blackList.add("23212f");
//		List<String> exeHeadList = new ArrayList<>();
//		exeHeadList.add("4d5a90");
//		whiteList.put("exe", exeHeadList);
        fileTypeMapInit();
    }

    public static void fileTypeMapInit() {
        fileTypeMap.put("ffd8ff", "jpg");
        fileTypeMap.put("89504e", "png");
        fileTypeMap.put("47494638", "gif");
        fileTypeMap.put("424d", "bmp");
        fileTypeMap.put("41564920", "avi");
        fileTypeMap.put("52494646", "avi");
        fileTypeMap.put("3026b2758e66cf11a6d9", "wmv");
        fileTypeMap.put("000000", "mp4");
        fileTypeMap.put("255044462d312e", "pdf");
        fileTypeMap.put("d0cf11e0a1b11ae10000", "doc");
        // docx|xlsx|pptx
        fileTypeMap.put("504b03041400", "docx");
        fileTypeMap.put("68746D6C3E", "html");
        fileTypeMap.put("3c21444f435459504520", "html");
        fileTypeMap.put("3c21646f637479706520", "htm");
        fileTypeMap.put("49492a", "tif");
        fileTypeMap.put("414331", "dwg");
        fileTypeMap.put("48544d", "css");
        fileTypeMap.put("696b2e", "js");
        fileTypeMap.put("7b5c72", "rtf");
        fileTypeMap.put("384250", "psd");
        fileTypeMap.put("46726f", "eml");
        fileTypeMap.put("537461", "mdb");
        fileTypeMap.put("252150", "ps");
        fileTypeMap.put("2e524d", "rmvb");
        // flv、f4v
        fileTypeMap.put("464c56", "flv");
        fileTypeMap.put("494433", "mp3");
        fileTypeMap.put("000001", "mpg");
        fileTypeMap.put("52494646e27807005741", "wav");
        // MIDI (mid)
        fileTypeMap.put("4d546864000000060001", "mid");
        fileTypeMap.put("504b0304140000000800", "zip");
        fileTypeMap.put("52617221", "rar");
        fileTypeMap.put("235468", "ini");
        fileTypeMap.put("504b03040a0000000000", "jar");
        fileTypeMap.put("4d5a90", "exe");
        fileTypeMap.put("3c2540", "jsp");
        fileTypeMap.put("4d616e", "mf");
        fileTypeMap.put("3C3F786D6C", "xml");
        fileTypeMap.put("494e53", "sql");
        fileTypeMap.put("706163", "java");
        fileTypeMap.put("406563", "bat");
        fileTypeMap.put("1f8b0800000000000000", "gz");
        fileTypeMap.put("6c6f67", "properties");
        fileTypeMap.put("cafeba", "class");
        fileTypeMap.put("495453", "chm");
        fileTypeMap.put("04000000010000001300", "mxp");
        fileTypeMap.put("6431303a637265617465", "torrent");
        fileTypeMap.put("6D6F6F76", "mov");
        fileTypeMap.put("FF575043", "wpd");
        fileTypeMap.put("CFAD12FEC5FD746F", "dbx");
        fileTypeMap.put("2142444E", "pst");
        fileTypeMap.put("AC9EBD8F", "qdf");
        fileTypeMap.put("E3828596", "pwl");
        fileTypeMap.put("2E7261FD", "ram");
        fileTypeMap.put("49492A00", "tif");

        fileTypeMap.put("23212f", "sh");
    }

    /**
     * 是否非法文件名或者路径
     *
     * @param basePath 限定路径（选择文件或者路径只能是在这下面） 可为空， 为空则不验证限定路径
     * @param filename 文件名或者路径  不可为空 为空则抛出异常 非法请求
     * @throws ServerException ServerException
     * @Title: isValidFilename
     */
    public static void isValidFilename(String basePath, String filename) throws ServerException {
        if (org.apache.commons.lang3.StringUtils.isBlank(filename)) {
            log.error("文件名或者路径是否非法的,filename = {}", filename);
            throw exception(COMMON_PARAM_ERROR);
        }
        String filenameType1 = "../";
        String filenameType2 = "..\\";
        if (filename.contains(filenameType1) || filename.contains(filenameType2) || (filename.indexOf(SPECIALS1) != -1)) {
            log.error("文件名或者路径是否非法的,包含../或者包含了..\\ filename = {}", filename);
            throw exception(COMMON_PARAM_ERROR);
        } else {
            if (org.apache.commons.lang3.StringUtils.isNoneBlank(basePath)) {
                if (!filename.startsWith(basePath)) {
                    log.error("文件名或者路径是否非法的,文件{} 不是以basePath前缀开始的,{}", filename, basePath);
                    throw exception(COMMON_PARAM_ERROR);
                }
            }
        }
    }

    /**
     * 是否压缩包格式
     *
     * @param ext 格式
     * @Title: isValidZipExt
     * @return: boolean true 是压缩包
     */
    public static boolean isValidZipExt(String ext) {
        ext = ext.toLowerCase(Locale.ENGLISH);
        for (String s : ZIP_EXT) {
            if (s.equalsIgnoreCase(ext)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断该文件是否允许上传！
     *
     * @param inputStream    检查输入流上传的文件是否合法
     * @param isTemplateFile 是否是模板,模板不限制黑名单html htm
     * @Title: checkFileIsValid
     * @return: Boolean
     */
    public static Boolean checkFileIsValid(InputStream inputStream, boolean isTemplateFile) {
        String suffix = null;
        String fileHeaderCode = null;
        Boolean flag = false;
        if (inputStream == null) {
            return false;
        }
        fileHeaderCode = getFileHeaderCode(inputStream);
        suffix = getFileSuffix(fileHeaderCode);
        boolean isInWhite = false;
        if (StringUtils.isNotBlank(suffix)) {
            suffix = suffix.toLowerCase(Locale.ENGLISH);
            isInWhite = whiteList.containsKey(suffix);
        }
        /** 没在白名单文件头中定义的则检查黑名单 可能是txt html等 */
        if (isInWhite) {
            if (codeIsBelongToExt(suffix, fileHeaderCode)) {
                flag = true;
            } else {
                flag = false;
            }
        } else {
            List<String> backs = blackList;
            if (isTemplateFile) {
                backs.removeAll(htmlHeaders);
            }
            for (String string : backs) {
                if (fileHeaderCode.toLowerCase(Locale.ENGLISH)
                        .startsWith(string.toLowerCase(Locale.ENGLISH))
                        || string.toLowerCase(Locale.ENGLISH)
                        .startsWith(fileHeaderCode.toLowerCase(Locale.ENGLISH))) {
                    flag = false;
                    break;
                } else {
                    flag = true;
                }
            }
        }
        return flag;
    }

    /**
     * 获取文件头
     *
     * @param file 輸入流
     * @Title: getFileHeaderCode
     * @return: String
     */
    private static String getFileHeaderCode(InputStream file) {
        byte[] b = new byte[10];
        String fileCode = "";
        try {
            file.read(b, 0, b.length);
            fileCode = bytesToHexString(b);
        } catch (IOException e) {
            log.error("IOException:", e);
        }
        return fileCode;
    }

    /**
     * 得到上传文件的文件头
     *
     * @param src 文件数组
     * @return
     */
    private static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (null == src || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * 白名单，判断文件头是否与文件后缀一致
     *
     * @param ext  文件后缀格式
     * @param code 文件头
     * @return
     */
    private static Boolean codeIsBelongToExt(String ext, String code) {
        Boolean flag = false;
        List<String> list = whiteList.get(ext);
        if (list != null && list.size() > 0) {
            for (String string : list) {
                if (code.toLowerCase(Locale.ENGLISH)
                        .startsWith(string.toLowerCase(Locale.ENGLISH))
                        || string.toLowerCase(Locale.ENGLISH)
                        .startsWith(code.toLowerCase(Locale.ENGLISH))) {
                    flag = true;
                }
            }
        }
        return flag;
    }

    /**
     * 获取文件类型
     *
     * @param fileHeaderCode 文件头
     * @return
     */
    private static String getFileSuffix(String fileHeaderCode) {
        String res = null;
        Iterator<String> keyIter = fileTypeMap.keySet().iterator();
        while (keyIter.hasNext()) {
            String key = keyIter.next();
            if (key.toLowerCase(Locale.ENGLISH).startsWith(fileHeaderCode.toLowerCase(Locale.ENGLISH))
                    || fileHeaderCode.toLowerCase(Locale.ENGLISH).startsWith(key)) {
                res = fileTypeMap.get(key);
                break;
            }
        }
        return res;
    }

    public static String getSize(@Nullable Long length) {
        if (length == null) {
            return "0 KB";
        }
        if (length <= KB) {
            return "1 KB";
        }
        long lengthKb = length / KB;

        if (lengthKb < THRESHOLD_900) {
            return lengthKb + " KB";
        }
        DecimalFormat format0 = new DecimalFormat("0");
        DecimalFormat format1 = new DecimalFormat("0.#");
        DecimalFormat format2 = new DecimalFormat("0.##");
        BigDecimal lengthMb = new BigDecimal(length).divide(new BigDecimal(KB * KB), 2, RoundingMode.HALF_DOWN);
        if (lengthMb.compareTo(new BigDecimal(THRESHOLD_10)) < 0) {
            return format2.format(lengthMb) + " MB";
        } else if (lengthMb.compareTo(new BigDecimal(THRESHOLD_100)) < 0) {
            return format1.format(lengthMb) + " MB";
        } else if (lengthMb.compareTo(new BigDecimal(THRESHOLD_900)) < 0) {
            return format0.format(lengthMb) + " MB";
        }
        BigDecimal lengthGb = new BigDecimal(length).divide(new BigDecimal(KB * KB * KB), 2, RoundingMode.HALF_DOWN);
        if (lengthGb.compareTo(new BigDecimal(THRESHOLD_10)) < 0) {
            return format2.format(lengthGb) + " GB";
        } else if (lengthGb.compareTo(new BigDecimal(THRESHOLD_100)) < 0) {
            return format1.format(lengthGb) + " GB";
        } else if (lengthGb.compareTo(new BigDecimal(THRESHOLD_900)) < 0) {
            return format0.format(lengthGb) + " GB";
        }
        BigDecimal lengthTb = new BigDecimal(length).divide(new BigDecimal(KB * KB * KB * KB), 2, RoundingMode.HALF_DOWN);
        if (lengthTb.compareTo(new BigDecimal(THRESHOLD_10)) < 0) {
            return format2.format(lengthTb) + " TB";
        } else if (lengthTb.compareTo(new BigDecimal(THRESHOLD_100)) < 0) {
            return format1.format(lengthTb) + " TB";
        } else {
            return format0.format(lengthTb) + " TB";
        }
    }

}
